import { deleteNearSelection } from './deleteNearSelection.js';
import { runInOp } from '../display/operations.js';
import { ensureCursorVisible } from '../display/scrolling.js';
import { endOfLine } from '../input/movement.js';
import { clipPos, Pos } from '../line/pos.js';
import { visualLine, visualLineEnd } from '../line/spans.js';
import { getLine, lineNo } from '../line/utils_line.js';
import { Range } from '../model/selection.js';
import { selectAll } from '../model/selection_updates.js';
import { countColumn, sel_dontScroll, sel_move, spaceStr } from '../util/misc.js';
import { getOrder } from '../util/bidi.js';

// Commands are parameter-less actions that can be performed on an
// editor, mostly used for keybindings.
export let commands = {
  selectAll: selectAll,
  singleSelection: cm => cm.setSelection(cm.getCursor('anchor'), cm.getCursor('head'), sel_dontScroll),
  killLine: cm =>
    deleteNearSelection(cm, range => {
      if (range.empty()) {
        let len = getLine(cm.doc, range.head.line).text.length;
        if (range.head.ch == len && range.head.line < cm.lastLine())
          return { from: range.head, to: Pos(range.head.line + 1, 0) };
        else return { from: range.head, to: Pos(range.head.line, len) };
      } else {
        return { from: range.from(), to: range.to() };
      }
    }),
  deleteLine: cm =>
    deleteNearSelection(cm, range => ({
      from: Pos(range.from().line, 0),
      to: clipPos(cm.doc, Pos(range.to().line + 1, 0)),
    })),
  delLineLeft: cm =>
    deleteNearSelection(cm, range => ({
      from: Pos(range.from().line, 0),
      to: range.from(),
    })),
  delWrappedLineLeft: cm =>
    deleteNearSelection(cm, range => {
      let top = cm.charCoords(range.head, 'div').top + 5;
      let leftPos = cm.coordsChar({ left: 0, top: top }, 'div');
      return { from: leftPos, to: range.from() };
    }),
  delWrappedLineRight: cm =>
    deleteNearSelection(cm, range => {
      let top = cm.charCoords(range.head, 'div').top + 5;
      let rightPos = cm.coordsChar({ left: cm.display.lineDiv.offsetWidth + 100, top: top }, 'div');
      return { from: range.from(), to: rightPos };
    }),
  undo: cm => cm.undo(),
  redo: cm => cm.redo(),
  undoSelection: cm => cm.undoSelection(),
  redoSelection: cm => cm.redoSelection(),
  goDocStart: cm => cm.extendSelection(Pos(cm.firstLine(), 0)),
  goDocEnd: cm => cm.extendSelection(Pos(cm.lastLine())),
  goLineStart: cm => cm.extendSelectionsBy(range => lineStart(cm, range.head.line), { origin: '+move', bias: 1 }),
  goLineStartSmart: cm => cm.extendSelectionsBy(range => lineStartSmart(cm, range.head), { origin: '+move', bias: 1 }),
  goLineEnd: cm => cm.extendSelectionsBy(range => lineEnd(cm, range.head.line), { origin: '+move', bias: -1 }),
  goLineRight: cm =>
    cm.extendSelectionsBy(range => {
      let top = cm.cursorCoords(range.head, 'div').top + 5;
      return cm.coordsChar({ left: cm.display.lineDiv.offsetWidth + 100, top: top }, 'div');
    }, sel_move),
  goLineLeft: cm =>
    cm.extendSelectionsBy(range => {
      let top = cm.cursorCoords(range.head, 'div').top + 5;
      return cm.coordsChar({ left: 0, top: top }, 'div');
    }, sel_move),
  goLineLeftSmart: cm =>
    cm.extendSelectionsBy(range => {
      let top = cm.cursorCoords(range.head, 'div').top + 5;
      let pos = cm.coordsChar({ left: 0, top: top }, 'div');
      if (pos.ch < cm.getLine(pos.line).search(/\S/)) return lineStartSmart(cm, range.head);
      return pos;
    }, sel_move),
  goLineUp: cm => cm.moveV(-1, 'line'),
  goLineDown: cm => cm.moveV(1, 'line'),
  goPageUp: cm => cm.moveV(-1, 'page'),
  goPageDown: cm => cm.moveV(1, 'page'),
  goCharLeft: cm => cm.moveH(-1, 'char'),
  goCharRight: cm => cm.moveH(1, 'char'),
  goColumnLeft: cm => cm.moveH(-1, 'column'),
  goColumnRight: cm => cm.moveH(1, 'column'),
  goWordLeft: cm => cm.moveH(-1, 'word'),
  goGroupRight: cm => cm.moveH(1, 'group'),
  goGroupLeft: cm => cm.moveH(-1, 'group'),
  goWordRight: cm => cm.moveH(1, 'word'),
  delCharBefore: cm => cm.deleteH(-1, 'codepoint'),
  delCharAfter: cm => cm.deleteH(1, 'char'),
  delWordBefore: cm => cm.deleteH(-1, 'word'),
  delWordAfter: cm => cm.deleteH(1, 'word'),
  delGroupBefore: cm => cm.deleteH(-1, 'group'),
  delGroupAfter: cm => cm.deleteH(1, 'group'),
  indentAuto: cm => cm.indentSelection('smart'),
  indentMore: cm => cm.indentSelection('add'),
  indentLess: cm => cm.indentSelection('subtract'),
  insertTab: cm => cm.replaceSelection('\t'),
  insertSoftTab: cm => {
    let spaces = [],
      ranges = cm.listSelections(),
      tabSize = cm.options.tabSize;
    for (let i = 0; i < ranges.length; i++) {
      let pos = ranges[i].from();
      let col = countColumn(cm.getLine(pos.line), pos.ch, tabSize);
      spaces.push(spaceStr(tabSize - (col % tabSize)));
    }
    cm.replaceSelections(spaces);
  },
  defaultTab: cm => {
    if (cm.somethingSelected()) cm.indentSelection('add');
    else cm.execCommand('insertTab');
  },
  // Swap the two chars left and right of each selection's head.
  // Move cursor behind the two swapped characters afterwards.
  //
  // Doesn't consider line feeds a character.
  // Doesn't scan more than one line above to find a character.
  // Doesn't do anything on an empty line.
  // Doesn't do anything with non-empty selections.
  transposeChars: cm =>
    runInOp(cm, () => {
      let ranges = cm.listSelections(),
        newSel = [];
      for (let i = 0; i < ranges.length; i++) {
        if (!ranges[i].empty()) continue;
        let cur = ranges[i].head,
          line = getLine(cm.doc, cur.line).text;
        if (line) {
          if (cur.ch == line.length) cur = new Pos(cur.line, cur.ch - 1);
          if (cur.ch > 0) {
            cur = new Pos(cur.line, cur.ch + 1);
            cm.replaceRange(
              line.charAt(cur.ch - 1) + line.charAt(cur.ch - 2),
              Pos(cur.line, cur.ch - 2),
              cur,
              '+transpose'
            );
          } else if (cur.line > cm.doc.first) {
            let prev = getLine(cm.doc, cur.line - 1).text;
            if (prev) {
              cur = new Pos(cur.line, 1);
              cm.replaceRange(
                line.charAt(0) + cm.doc.lineSeparator() + prev.charAt(prev.length - 1),
                Pos(cur.line - 1, prev.length - 1),
                cur,
                '+transpose'
              );
            }
          }
        }
        newSel.push(new Range(cur, cur));
      }
      cm.setSelections(newSel);
    }),
  newlineAndIndent: cm =>
    runInOp(cm, () => {
      let sels = cm.listSelections();
      for (let i = sels.length - 1; i >= 0; i--)
        cm.replaceRange(cm.doc.lineSeparator(), sels[i].anchor, sels[i].head, '+input');
      sels = cm.listSelections();
      for (let i = 0; i < sels.length; i++) cm.indentLine(sels[i].from().line, null, true);
      ensureCursorVisible(cm);
    }),
  openLine: cm => cm.replaceSelection('\n', 'start'),
  toggleOverwrite: cm => cm.toggleOverwrite(),
};

function lineStart(cm, lineN) {
  let line = getLine(cm.doc, lineN);
  let visual = visualLine(line);
  if (visual != line) lineN = lineNo(visual);
  return endOfLine(true, cm, visual, lineN, 1);
}
function lineEnd(cm, lineN) {
  let line = getLine(cm.doc, lineN);
  let visual = visualLineEnd(line);
  if (visual != line) lineN = lineNo(visual);
  return endOfLine(true, cm, line, lineN, -1);
}
function lineStartSmart(cm, pos) {
  let start = lineStart(cm, pos.line);
  let line = getLine(cm.doc, start.line);
  let order = getOrder(line, cm.doc.direction);
  if (!order || order[0].level == 0) {
    let firstNonWS = Math.max(start.ch, line.text.search(/\S/));
    let inWS = pos.line == start.line && pos.ch <= firstNonWS && pos.ch;
    return Pos(start.line, inWS ? 0 : firstNonWS, start.sticky);
  }
  return start;
}
